<template>
  <div
    :class="{
      [`${prefixClass}-datepicker`]: true,
      [`${prefixClass}-datepicker-range`]: range,
      [`${prefixClass}-datepicker-inline`]: inline,
      disabled: disabled,
    }"
  >
    <div v-if="!inline" :class="`${prefixClass}-input-wrapper`" @mousedown="openPopup">
      <slot
        name="input"
        :props="{
          name: 'date',
          type: 'text',
          autocomplete: 'off',
          value: text,
          class: inputClass,
          readonly: !editable,
          disabled,
          placeholder,
          ...inputAttr,
        }"
        :events="{
          keydown: handleInputKeydown,
          focus: handleInputFocus,
          blur: handleInputBlur,
          input: handleInputInput,
          change: handleInputChange,
        }"
      >
        <input
          ref="input"
          v-bind="{
            name: 'date',
            type: 'text',
            autocomplete: 'off',
            value: text,
            class: inputClass,
            readonly: !editable,
            disabled,
            placeholder,
            ...inputAttr,
          }"
          v-on="{
            keydown: handleInputKeydown,
            focus: handleInputFocus,
            blur: handleInputBlur,
            input: handleInputInput,
            change: handleInputChange,
          }"
        />
      </slot>
      <i v-if="showClearIcon" :class="`${prefixClass}-icon-clear`" @mousedown.stop="handleClear">
        <slot name="icon-clear">
          <icon-close></icon-close>
        </slot>
      </i>
      <i :class="`${prefixClass}-icon-calendar`">
        <slot name="icon-calendar">
          <icon-calendar></icon-calendar>
        </slot>
      </i>
    </div>
    <Popup
      ref="popup"
      :class="popupClass"
      :style="popupStyle"
      :inline="inline"
      :visible="popupVisible"
      :append-to-body="appendToBody"
      @clickoutside="handleClickOutSide"
    >
      <div
        v-if="hasSlot('sidebar') || shortcuts.length"
        :class="`${prefixClass}-datepicker-sidebar`"
      >
        <slot name="sidebar" :value="currentValue" :emit="emitValue"></slot>
        <button
          v-for="(v, i) in shortcuts"
          :key="i"
          type="button"
          :class="`${prefixClass}-btn ${prefixClass}-btn-text ${prefixClass}-btn-shortcut`"
          @click="handleSelectShortcut(v)"
        >
          {{ v.text }}
        </button>
      </div>
      <div :class="`${prefixClass}-datepicker-content`">
        <div v-if="hasSlot('header')" :class="`${prefixClass}-datepicker-header`">
          <slot name="header" :value="currentValue" :emit="emitValue"></slot>
        </div>
        <div :class="`${prefixClass}-datepicker-body`">
          <slot name="content" :value="currentValue" :emit="emitValue">
            <component
              :is="currentComponent"
              ref="picker"
              v-bind="currentComponentProps"
              @select="handleSelectDate"
            ></component>
          </slot>
        </div>
        <div v-if="hasSlot('footer') || confirm" :class="`${prefixClass}-datepicker-footer`">
          <slot name="footer" :value="currentValue" :emit="emitValue"></slot>
          <button
            v-if="confirm"
            type="button"
            :class="`${prefixClass}-btn ${prefixClass}-datepicker-btn-confirm`"
            @click="handleConfirmDate"
          >
            {{ confirmText }}
          </button>
        </div>
      </div>
    </Popup>
  </div>
</template>

<script>
import { parse, format, getWeek } from 'date-format-parse';
import { isValidDate, isValidRangeDate, isValidDates } from './util/date';
import { pick, isObject, mergeDeep } from './util/base';
import { getLocale, getLocaleFieldValue } from './locale';
import Popup from './popup';
import IconCalendar from './icon/icon-calendar';
import IconClose from './icon/icon-close';
import CalendarPanel from './calendar/calendar-panel';
import CalendarRange from './calendar/calendar-range';
import TimePanel from './time/time-panel';
import TimeRange from './time/time-range';
import DatetimePanel from './datetime/datetime-panel';
import DatetimeRange from './datetime/datetime-range';

const componentMap = {
  default: CalendarPanel,
  time: TimePanel,
  datetime: DatetimePanel,
};
const componentRangeMap = {
  default: CalendarRange,
  time: TimeRange,
  datetime: DatetimeRange,
};

export default {
  name: 'DatePicker',
  components: {
    IconCalendar,
    IconClose,
    Popup,
  },
  provide() {
    return {
      translateFn: this.getLocaleFieldValue,
      getWeek: this.getWeek,
      prefixClass: this.prefixClass,
    };
  },
  props: {
    ...DatetimePanel.props,
    value: {},
    valueType: {
      type: String,
      default: 'date', // date, format, timestamp, or token like 'YYYY-MM-DD'
    },
    type: {
      type: String, // ['date', 'datetime', 'time', 'year', 'month', 'week']
      default: 'date',
    },
    format: {
      type: String,
      default() {
        const map = {
          date: 'YYYY-MM-DD',
          datetime: 'YYYY-MM-DD HH:mm:ss',
          year: 'YYYY',
          month: 'YYYY-MM',
          time: 'HH:mm:ss',
          week: 'w',
        };
        return map[this.type] || map.date;
      },
    },
    formatter: {
      type: Object,
    },
    range: {
      type: Boolean,
      default: false,
    },
    multiple: {
      type: Boolean,
      default: false,
    },
    rangeSeparator: {
      type: String,
      default() {
        return this.multiple ? ',' : ' ~ ';
      },
    },
    lang: {
      type: [String, Object],
    },
    placeholder: {
      type: String,
      default: '',
    },
    editable: {
      type: Boolean,
      default: true,
    },
    disabled: {
      type: Boolean,
      default: false,
    },
    clearable: {
      type: Boolean,
      default: true,
    },
    prefixClass: {
      type: String,
      default: 'mx',
    },
    inputClass: {
      default() {
        return `${this.prefixClass}-input`;
      },
    },
    inputAttr: {
      type: Object,
      default() {
        return {};
      },
    },
    appendToBody: {
      type: Boolean,
      default: true,
    },
    open: {
      type: Boolean,
      default: undefined,
    },
    popupClass: {},
    popupStyle: {
      type: Object,
      default: () => {
        return {};
      },
    },
    inline: {
      type: Boolean,
      default: false,
    },
    confirm: {
      type: Boolean,
      default: false,
    },
    confirmText: {
      type: String,
      default: 'OK',
    },
    renderInputText: {
      type: Function,
    },
    shortcuts: {
      type: Array,
      validator(value) {
        return (
          Array.isArray(value) &&
          value.every(
            v => isObject(v) && typeof v.text === 'string' && typeof v.onClick === 'function'
          )
        );
      },
      default() {
        return [];
      },
    },
  },
  data() {
    return {
      // cache the innervalue, wait to confirm
      currentValue: null,
      userInput: null,
      defaultOpen: false,
    };
  },
  computed: {
    currentComponent() {
      const map = this.range ? componentRangeMap : componentMap;
      return map[this.type] || map.default;
    },
    currentComponentProps() {
      const props = {
        ...pick(this, Object.keys(this.currentComponent.props)),
        value: this.currentValue,
      };
      return props;
    },
    popupVisible() {
      return !this.disabled && (typeof this.open === 'boolean' ? this.open : this.defaultOpen);
    },
    innerValue() {
      let { value } = this;
      if (this.validMultipleType) {
        value = Array.isArray(value) ? value : [];
        return value.map(this.value2date);
      }
      if (this.range) {
        value = Array.isArray(value) ? value.slice(0, 2) : [null, null];
        return value.map(this.value2date);
      }
      return this.value2date(this.value);
    },
    text() {
      if (this.userInput !== null) {
        return this.userInput;
      }
      if (typeof this.renderInputText === 'function') {
        return this.renderInputText(this.innerValue);
      }
      if (!this.isValidValue(this.innerValue)) {
        return '';
      }
      const fmt = this.format;
      if (Array.isArray(this.innerValue)) {
        return this.innerValue.map(v => this.formatDate(v, fmt)).join(this.rangeSeparator);
      }
      return this.formatDate(this.innerValue, fmt);
    },
    showClearIcon() {
      return !this.disabled && this.clearable && this.text;
    },
    locale() {
      if (isObject(this.lang)) {
        return mergeDeep(getLocale(), this.lang);
      }
      return getLocale(this.lang);
    },
    validMultipleType() {
      const types = ['date', 'month', 'year'];
      return this.multiple && !this.range && types.indexOf(this.type) !== -1;
    },
  },
  watch: {
    innerValue: {
      immediate: true,
      handler(val) {
        this.currentValue = val;
      },
    },
  },
  created() {
    if (typeof this.format === 'object') {
      console.warn(
        "[vue2-datepicker]: The prop `format` don't support Object any more. You can use the new prop `formatter` to replace it"
      );
    }
  },
  methods: {
    handleClickOutSide(evt) {
      const { target } = evt;
      if (!this.$el.contains(target)) {
        this.closePopup();
      }
    },
    getFormatter(key) {
      return (
        (isObject(this.formatter) && this.formatter[key]) ||
        (isObject(this.format) && this.format[key])
      );
    },
    getWeek(date, options) {
      if (typeof this.getFormatter('getWeek') === 'function') {
        return this.getFormatter('getWeek')(date, options);
      }
      return getWeek(date, options);
    },
    parseDate(value, fmt) {
      if (typeof this.getFormatter('parse') === 'function') {
        return this.getFormatter('parse')(value, fmt);
      }
      const backupDate = new Date();
      return parse(value, fmt, { locale: this.locale.formatLocale, backupDate });
    },
    formatDate(date, fmt) {
      if (typeof this.getFormatter('stringify') === 'function') {
        return this.getFormatter('stringify')(date, fmt);
      }
      return format(date, fmt, { locale: this.locale.formatLocale });
    },
    // transform the outer value to inner date
    value2date(value) {
      switch (this.valueType) {
        case 'date':
          return value instanceof Date ? new Date(value.getTime()) : new Date(NaN);
        case 'timestamp':
          return typeof value === 'number' ? new Date(value) : new Date(NaN);
        case 'format':
          return typeof value === 'string' ? this.parseDate(value, this.format) : new Date(NaN);
        default:
          return typeof value === 'string' ? this.parseDate(value, this.valueType) : new Date(NaN);
      }
    },
    // transform the inner date to outer value
    date2value(date) {
      if (!isValidDate(date)) return null;
      switch (this.valueType) {
        case 'date':
          return date;
        case 'timestamp':
          return date.getTime();
        case 'format':
          return this.formatDate(date, this.format);
        default:
          return this.formatDate(date, this.valueType);
      }
    },
    emitValue(date, type) {
      // fix IE11/10 trigger input event when input is focused. (placeholder !== '')
      this.userInput = null;
      const value = Array.isArray(date) ? date.map(this.date2value) : this.date2value(date);
      this.$emit('input', value);
      this.$emit('change', value, type);
      this.afterEmitValue(type);
      return value;
    },
    afterEmitValue(type) {
      // this.type === 'datetime', click the time should close popup
      if (!type || type === this.type || type === 'time') {
        this.closePopup();
      }
    },
    isValidValue(value) {
      if (this.validMultipleType) {
        return isValidDates(value);
      }
      if (this.range) {
        return isValidRangeDate(value);
      }
      return isValidDate(value);
    },
    isValidValueAndNotDisabled(value) {
      if (!this.isValidValue(value)) {
        return false;
      }
      const disabledDate =
        typeof this.disabledDate === 'function' ? this.disabledDate : () => false;
      const disabledTime =
        typeof this.disabledTime === 'function' ? this.disabledTime : () => false;
      if (!Array.isArray(value)) {
        value = [value];
      }
      return value.every(v => !disabledDate(v) && !disabledTime(v));
    },
    handleMultipleDates(date, dates) {
      if (this.validMultipleType && dates) {
        const nextDates = dates.filter(v => v.getTime() !== date.getTime());
        if (nextDates.length === dates.length) {
          nextDates.push(date);
        }
        return nextDates;
      }
      return date;
    },
    handleSelectDate(val, type, dates) {
      val = this.handleMultipleDates(val, dates);
      if (this.confirm) {
        this.currentValue = val;
      } else {
        this.emitValue(val, this.validMultipleType ? `multiple-${type}` : type);
      }
    },
    handleClear() {
      this.emitValue(this.range ? [null, null] : null);
      this.$emit('clear');
    },
    handleConfirmDate() {
      const value = this.emitValue(this.currentValue);
      this.$emit('confirm', value);
    },
    handleSelectShortcut(item) {
      if (isObject(item) && typeof item.onClick === 'function') {
        const date = item.onClick(this);
        if (date) {
          this.emitValue(date);
        }
      }
    },
    openPopup(evt) {
      if (this.popupVisible) return;
      this.defaultOpen = true;
      this.$emit('open', evt);
      this.$emit('update:open', true);
    },
    closePopup() {
      if (!this.popupVisible) return;
      this.defaultOpen = false;
      this.$emit('close');
      this.$emit('update:open', false);
    },
    blur() {
      // when use slot input
      if (this.$refs.input) {
        this.$refs.input.blur();
      }
    },
    focus() {
      if (this.$refs.input) {
        this.$refs.input.focus();
      }
    },
    handleInputChange() {
      if (!this.editable || this.userInput === null) return;
      const text = this.userInput.trim();
      this.userInput = null;
      if (text === '') {
        this.handleClear();
        return;
      }
      let date;
      if (this.validMultipleType) {
        date = text.split(this.rangeSeparator).map(v => this.parseDate(v.trim(), this.format));
      } else if (this.range) {
        let arr = text.split(this.rangeSeparator);
        if (arr.length !== 2) {
          // Maybe the separator during the day is the same as the separator for the date
          // eg: 2019-10-09-2020-01-02
          arr = text.split(this.rangeSeparator.trim());
        }
        date = arr.map(v => this.parseDate(v.trim(), this.format));
      } else {
        date = this.parseDate(text, this.format);
      }
      if (this.isValidValueAndNotDisabled(date)) {
        this.emitValue(date);
        this.blur();
      } else {
        this.$emit('input-error', text);
      }
    },
    handleInputInput(evt) {
      this.userInput = evt.target.value;
    },
    handleInputKeydown(evt) {
      const { keyCode } = evt;
      // Tab 9 or Enter 13
      if (keyCode === 9) {
        this.closePopup();
      } else if (keyCode === 13) {
        this.handleInputChange();
      }
    },
    handleInputBlur(evt) {
      // tab close
      this.$emit('blur', evt);
    },
    handleInputFocus(evt) {
      this.openPopup(evt);
      this.$emit('focus', evt);
    },
    hasSlot(name) {
      return !!(this.$slots[name] || this.$scopedSlots[name]);
    },
    getLocaleFieldValue(path) {
      return getLocaleFieldValue(path, this.locale);
    },
  },
};
</script>
